package cn.lingyangwl.framework.sign;

import cn.lingyangwl.framework.sign.core.HashAlgoProcess;
import cn.lingyangwl.framework.tool.core.StringUtils;
import cn.lingyangwl.framework.tool.core.exception.BaseException;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;

import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

/**
 * 签名工具
 * @author shenguangyang
 */
@Slf4j
public class SignUtils {
    /**
     * 签名url有效时间为15s
     */
    private static final Integer URL_TIMEOUT_SECONDS = 15;

    /**
     * 检验签名
     */
    public static void checkSign(SignPayload signPayload, String secretKey, Object data) {
        checkSign(signPayload, true, secretKey, data);
    }

    /**
     * 检验签名
     * @param isCheckTimeOut 是否检查超时时间
     */
    public static void checkSign(SignPayload signPayload, boolean isCheckTimeOut, String secretKey, Object data) {
        try {
            if (Objects.isNull(data) || Objects.isNull(signPayload)) {
                return;
            }

            signPayload.check();

            // 验证url是否超时
            if (isCheckTimeOut) {
                checkUrlTimeOut(signPayload);
            }

            // 校验签名
            doCheckSign(signPayload, secretKey, data);

        } catch (Exception e) {
            if (e instanceof BaseException) {
                throw (BaseException) e;
            }
            log.error("signPayload: {}", JSON.toJSONString(signPayload));
            throw new SignException("签名校验失败");
        }
    }

    /**
     * 完成生成签名 <br/>
     * 因为在实际生成签名的时候, 可能因为日期字段格式化方式不一样, 当前生成签名默认格式化日期格式为 yyyy-MM-dd HH:mm:ss.SSS,
     * 然后当你在将数据转成body字符串的时候, 使用框架不同或者你手动指定了日期格式化方式为其他的格式, 则会导致, 生成签名签名时候使用
     * 的请求数据和实际传递的数据不一致, 最终导致接收方验签失败, 比如 <br/>
     *
     * 签名使用的数据: belongInfoId=1727077764818133&birthday=2023-03-28 22:25:08.264 <br/>
     * 实际请求body: belongInfoId=1727077764818133&birthday=2023-03-28
     *
     * 所以, 推荐将需要签名的接口, 所有入参为日期格式的都以字符串方式或者时间戳方式传递
     */
    public static SignPayload genSign(SignType signType, String secretId, String secretKey, Object data) throws Exception {
        if (Objects.isNull(data)) {
            return null;
        }

        StopWatch watch = StopWatch.createStarted();
        String ts = String.valueOf(System.currentTimeMillis());
        // 随机数，生成随机数主要保证签名的多变性
        String nonce = RandomUtil.randomString(32);

        JSONObject dataMap = (JSONObject) JSON.toJSON(data);
        SignPayload signPayload = SignPayload.builder()
                .signType(signType.getType())
                .secretId(secretId).nonce(nonce).timeStamp(ts).build();
        dataMap.putAll(signPayload.toMapOfObjectType());
        String sortParams = sortParams(dataMap, secretId, secretKey);
        String sign = HashAlgoProcess.getAlgoProcess(signType).process(sortParams);
        log.debug("genSign ===> time: {} ms, sign: {}, signData: {}", watch.getTime(TimeUnit.MILLISECONDS), sign, sortParams);
        watch.stop();

        signPayload.setSign(sign);
        return signPayload;
    }

    private static void checkUrlTimeOut(SignPayload signPayload) {
        String startTimeStamp = signPayload.getTimeStamp();

        long timeOut = 1000L * URL_TIMEOUT_SECONDS;
        Instant startTime = Instant.ofEpochMilli(Long.parseLong(startTimeStamp));

        if (Duration.between(startTime, Instant.now()).abs().toMillis() >= timeOut) {
            log.warn("请求从客户端到服务端所用的时间超出 {} ms", timeOut);
            throw new SignException("签名url请求超时");
        }
    }

    private static void doCheckSign(SignPayload signPayload, String secretKey, Object data) throws Exception {
        JSONObject dataMap = (JSONObject) JSON.toJSON(data);
        dataMap.putAll(signPayload.toMapOfObjectType());

        String sign = signPayload.getSign();
        if (StringUtils.isEmpty(sign)) {
            throw new SignException("签名校验失败-签名为空");
        }
        StopWatch watch = StopWatch.createStarted();

        String signData = sortParams(dataMap, signPayload.getSecretId(), secretKey);
        String genSign = HashAlgoProcess.getAlgoProcess(SignType.ofByType(signPayload.getSignType())).process(signData);
        log.debug("checkSign ==> time: {} ms, sign: {}, signData: {}", watch.getTime(TimeUnit.MILLISECONDS), sign, signData);
        watch.stop();

        if (!sign.equals(genSign)) {
            throw new SignException("签名校验失败");
        }
    }

    private static String sortParams(Map<String, Object> params, String secretId, String secretKey) {
        try {
            params.remove(SignPayload.SIGN);
            params.put(SignPayload.SECRET_ID, secretId);
            params.put(SignPayload.SECRET_KEY, secretKey);

            StringBuilder sb = new StringBuilder();
            // 先对请求参数排序
            TreeSet<String> sortSet = new TreeSet<>(params.keySet());
            // 把参数的key value链接起来 secretKey放在最后面，得到要加密的字符串
            for (String key : sortSet) {
                Object obj = params.get(key);
                String value = "";
                if (Objects.isNull(obj) || StringUtils.isEmpty(String.valueOf(obj))) {
                    continue;
                }

                if (isJavaClass(obj.getClass())) {
                    value = String.valueOf(parseDate(obj));
                } else {
                    value = JSON.toJSONString(obj, JSONWriter.Feature.MapSortField, JSONWriter.Feature.WriteEnumsUsingName);
                }
                sb.append(key).append("=").append(value.replace(" ", "")).append("&");
            }
            return sb.toString();
        } finally {
            for (String key : SignPayload.getAllPayload()) {
                params.remove(key);
            }
        }
    }

    /**
     * 解析日期
     */
    public static Object parseDate(Object obj) {
        if (obj instanceof LocalDateTime) {
            LocalDateTime localDateTime = (LocalDateTime) obj;
            return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
        } else if (obj instanceof LocalDate) {
            LocalDate localDate = (LocalDate) obj;
            return localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        } else if (obj instanceof LocalTime) {
            LocalTime localTime = (LocalTime) obj;
            return localTime.format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));
        } else if (obj instanceof Date) {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            return format.format(obj);
        }
        return obj;
    }

    private static boolean isJavaClass(Class<?> clz) {
        return clz.getClassLoader() == null;
    }
}
